﻿using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;
using ifunction.JPush.V3;

namespace ifunction.JPush
{
    /// <summary>
    /// Abstract class JPushService. You need to create a class and inherit <c>JPushService</c> and override method <c>ReportException</c>.
    /// <example>
    /// A sample is as following:
    /// <code>
    ///    public class LocalJPushService : JPushService
    ///    {
    ///        public LocalJPushService()
    ///            : base(Settings.Default.JPushAppKey, Settings.Default.JPushMasterSecret, Settings.Default.SqlConnection)
    ///        {
    ///             // You can keep those parameters as constructor parameters or make them be from configuration.
    ///             // Anyway, the base class need them when construct it.
    ///        }
    ///
    ///        protected override void ReportException(Exception exception)
    ///        {
    ///             // You can throw it directly or handle it.
    ///            throw exception;
    ///        }
    ///    }
    /// </code>
    /// </example>
    /// </summary>
    public abstract class JPushService
    {
        /// <summary>
        /// The handler
        /// </summary>
        protected JPushHandler handler = null;

        /// <summary>
        /// The database operator
        /// </summary>
        protected DatabaseOperator databaseOperator = null;

        /// <summary>
        /// Initializes a new instance of the <see cref="JPushService"/> class.
        /// </summary>
        /// <param name="appKey">The application key.</param>
        /// <param name="masterSecret">The master secret.</param>
        public JPushService(string appKey, string masterSecret, string sqlConnection)
        {
            databaseOperator = new DatabaseOperator(sqlConnection);
            handler = new JPushHandler(appKey, masterSecret, CreatePushMessageLog, UpdatePushMessageStatus, InitializeMessageTrackingId, ReportException, 30);
        }

        /// <summary>
        /// Sends the push message.
        /// </summary>
        /// <param name="request">The request.</param>
        /// <returns>PushResponse.</returns>
        public PushResponse SendPushMessage(PushMessageRequestV3 request)
        {
            return handler.SendPushMessage(request);
        }

        /// <summary>
        /// Queries the push message status.
        /// </summary>
        /// <returns>List{PushMessageStatus}.</returns>
        public List<PushMessageStatus> QueryPushMessageStatus(PushMessageCriteria criteria)
        {
            const string spName = "sp_QueryJPushMessageLogV3";

            try
            {
                List<SqlParameter> parameters = new List<SqlParameter>();
                parameters.Add(GenerateSqlSpParameter("MessageId", criteria.MessageId));
                parameters.Add(GenerateSqlSpParameter("ReceiverValue", criteria.ReceiverValue));
                parameters.Add(GenerateSqlSpParameter("Content", criteria.Content));
                parameters.Add(GenerateSqlSpParameter("Platform", criteria.Platform.EnumToInt32()));
                parameters.Add(GenerateSqlSpParameter("IsTest", criteria.IsTestEnvironment));
                parameters.Add(GenerateSqlSpParameter("OrderDesc", criteria.OrderDesc));

                var reader = databaseOperator.ExecuteReader(spName, parameters);
                return ReadPushMessageStatus(reader);
            }
            catch (Exception ex)
            {
                ReportException(new InvalidOperationException("Failed to query push message log.", ex));
                return null;
            }
        }

        /// <summary>
        /// Queries the push message log.
        /// </summary>
        /// <param name="criteria">The criteria.</param>
        /// <returns>List{PushMessageStatus}.</returns>
        /// <exception cref="System.InvalidOperationException">Failed to initialize message tracking id.</exception>
        public List<PushMessageLog> QueryPushMessageLog(PushMessageCriteria criteria)
        {
            const string spName = "sp_QueryJPushMessageLogV3";

            try
            {
                var parameters = new List<SqlParameter>
                {
                    GenerateSqlSpParameter("MessageId", criteria.MessageId),
                    GenerateSqlSpParameter("ReceiverValue", criteria.ReceiverValue),
                    GenerateSqlSpParameter("Content", criteria.Content),
                    GenerateSqlSpParameter("Platform", criteria.Platform.EnumToInt32()),
                    GenerateSqlSpParameter("FromUtc", criteria.FromUtcStamp),
                    GenerateSqlSpParameter("ToUtc", criteria.ToUtcStamp),
                    GenerateSqlSpParameter("IsTest", criteria.IsTestEnvironment),
                    GenerateSqlSpParameter("OrderDesc", criteria.OrderDesc)
                };

                var reader = databaseOperator.ExecuteReader(spName, parameters);
                return ReadPushMessageLog(reader);
            }
            catch (Exception ex)
            {
                ReportException(new InvalidOperationException("Failed to query push message log.", ex));
                return null;
            }
        }

        /// <summary>
        /// Creates the push message log.
        /// </summary>
        /// <param name="request">The request.</param>
        /// <param name="response">The response.</param>
        /// <exception cref="System.InvalidOperationException">Failed to create push message log.</exception>
        protected void CreatePushMessageLog(PushMessageRequestV3 request, PushResponse response)
        {
            if (request != null && response != null && !string.IsNullOrWhiteSpace(response.MessageId))
            {
                try
                {
                    PushMessageLog pushMessageLog = new PushMessageLog
                    {
                        IsTestEnvironment = request.IsTestEnvironment,
                        AppMessage = request.AppMessage,
                        Notification = request.Notification,
                        Platform = request.Platform,
                        MessageId = response.MessageId,
                        Audience = request.Audience,
                        ResponseCode = response.ResponseCode,
                        ResponseMessage = response.ResponseMessage,
                        AndroidDeliveredCount = null,
                        ApplePushNotificationDeliveredCount = null,
                        WindowsPhoneDeliveredCount = null
                    };
                    this.CreateOrUpdateJPushMessageLog(pushMessageLog);
                }
                catch (Exception ex)
                {
                    throw new InvalidOperationException("Failed to create push message log.", ex);
                }
            }
        }

        /// <summary>
        /// Updates the push message status.
        /// </summary>
        /// <param name="messageStatusList">The message status list.</param>
        protected void UpdatePushMessageStatus(List<PushMessageStatus> messageStatusList)
        {
            if (messageStatusList != null && messageStatusList.Count > 0)
            {
                try
                {
                    foreach (var one in messageStatusList)
                    {
                        var log = new PushMessageLog
                        {
                            MessageId = one.MessageId.ToString(),
                            AndroidDeliveredCount = one.AndroidDeliveredCount,
                            ApplePushNotificationDeliveredCount = one.ApplePushNotificationDeliveredCount
                        };

                        CreateOrUpdateJPushMessageLog(log);
                    }
                }
                catch (Exception ex)
                {
                    ReportException(new InvalidOperationException("Failed to update push message status.", ex));
                }
            }
        }

        /// <summary>
        /// Creates the or update j push message log.
        /// </summary>
        /// <param name="messageLog">The message log.</param>
        /// <exception cref="System.ArgumentNullException">
        /// Object [messageLog] is null for operate.
        /// or
        /// Object [messageLog.Message] is null for operate.
        /// </exception>
        protected void CreateOrUpdateJPushMessageLog(PushMessageLog messageLog)
        {
            const string spName = "sp_CreateOrUpdateJPushMessageLogV3";

            try
            {
                if (messageLog == null)
                {
                    throw new ArgumentNullException("Object [messageLog] is null for operate.");
                }

                // If it is new (message id is null or empty), message can not be null
                if (string.IsNullOrWhiteSpace(messageLog.MessageId) && (messageLog.Notification == null && messageLog.AppMessage == null))
                {
                    throw new ArgumentNullException("Object [messageLog.Message/Notification] is null for operate.");
                }

                List<SqlParameter> parameters = new List<SqlParameter>();
                parameters.Add(GenerateSqlSpParameter("MessageId", messageLog.MessageId));
                parameters.Add(GenerateSqlSpParameter("Audience", messageLog.Audience == null ? null : messageLog.Audience.ToPureXml()));
                parameters.Add(GenerateSqlSpParameter("Platform", (int)messageLog.Platform));
                parameters.Add(GenerateSqlSpParameter("AppMessage", messageLog.AppMessage == null ? null : messageLog.AppMessage.ToPureXml()));
                parameters.Add(GenerateSqlSpParameter("Notification", messageLog.Notification == null ? null : messageLog.Notification.ToPureXml()));
                parameters.Add(GenerateSqlSpParameter("ResponseCode", (int)messageLog.ResponseCode));
                parameters.Add(GenerateSqlSpParameter("ResponseMessage", messageLog.ResponseMessage));
                parameters.Add(GenerateSqlSpParameter("AndroidDeliveredCount", messageLog.AndroidDeliveredCount));
                parameters.Add(GenerateSqlSpParameter("ApplePushNotificationDeliveredCount", messageLog.ApplePushNotificationDeliveredCount));
                parameters.Add(GenerateSqlSpParameter("WindowsPhonePushNotificationDeliveredCount", messageLog.WindowsPhoneDeliveredCount));
                parameters.Add(GenerateSqlSpParameter("IsTest", messageLog.IsTestEnvironment));

                var result = databaseOperator.ExecuteNonQuery(spName, parameters);
            }
            catch (Exception ex)
            {
                ReportException(new InvalidOperationException("Failed to CreateOrUpdateJPushMessageLog.", ex));
            }
        }

        /// <summary>
        /// Initializes the message tracking identifier.
        /// </summary>
        /// <param name="fromUtcDateStamp">From UTC date stamp.</param>
        /// <returns>IEnumerable{System.String}.</returns>
        protected IEnumerable<string> InitializeMessageTrackingId()
        {
            const string spName = "sp_GetActiveJPushMessage";

            try
            {
                var reader = databaseOperator.ExecuteReader(spName);
                return ReadTrackingId(reader);
            }
            catch (Exception ex)
            {
                ReportException(new InvalidOperationException("Failed to initialize message tracking id.", ex));
                return null;
            }
        }

        /// <summary>
        /// Reports the exception.
        /// </summary>
        /// <param name="exception">The exception.</param>
        public abstract void ReportException(Exception exception);

        /// <summary>
        /// Reads the tracking identifier.
        /// </summary>
        /// <param name="reader">The reader.</param>
        /// <returns>List&lt;System.String&gt;.</returns>
        protected static List<string> ReadTrackingId(SqlDataReader reader)
        {
            List<string> result = new List<string>();

            if (reader != null)
            {
                try
                {
                    while (reader.Read())
                    {
                        result.Add(reader["MessageId"].DBToString());
                    }
                }
                catch (Exception ex)
                {
                    throw ex;
                }
                finally
                {
                    reader.Close();
                }
            }

            return result;
        }

        /// <summary>
        /// Reads the push message log.
        /// </summary>
        /// <param name="reader">The reader.</param>
        /// <returns>List&lt;PushMessageLog&gt;.</returns>
        protected static List<PushMessageLog> ReadPushMessageLog(SqlDataReader reader)
        {
            List<PushMessageLog> result = new List<PushMessageLog>();

            if (reader != null)
            {
                try
                {
                    while (reader.Read())
                    {
                        PushMessageLog log = new PushMessageLog
                        {
                            Key = reader["Key"].DBToGuid().Value,
                            MessageId = reader["MessageId"].DBToString(),
                            Platform = (PushPlatform)reader["Platform"].DBToInt32(),
                            ResponseCode = (PushResponseCode)reader["ResponseCode"].DBToInt32(),
                            ResponseMessage = reader["ResponseMessage"].DBToString(),
                            AndroidDeliveredCount = reader["AndroidDeliveredCount"].DBToNullableInt32(),
                            ApplePushNotificationDeliveredCount = reader["ApplePushNotificationDeliveredCount"].DBToNullableInt32(),
                            WindowsPhoneDeliveredCount = reader["WindowsPhonePushNotificationDeliveredCount"].DBToNullableInt32(),
                            IsTestEnvironment = reader["IsTest"].DBToBoolean(),
                            CreatedStamp = reader["CreatedStamp"].DBToDateTime().Value,
                            LastUpdatedStamp = reader["LastUpdatedStamp"].DBToDateTime().Value
                        };

                        var xml = reader["Audience"].DBToString();
                        if (!string.IsNullOrWhiteSpace(xml))
                        {
                            log.Audience = XElement.Parse(xml).PureXmlToAudience();
                        }

                        xml = reader["AppMessage"].DBToString();
                        if (!string.IsNullOrWhiteSpace(xml))
                        {
                            log.AppMessage = XElement.Parse(xml).PureXmlToAppMessage();
                        }

                        xml = reader["Notification"].DBToString();
                        if (!string.IsNullOrWhiteSpace(xml))
                        {
                            log.Notification = XElement.Parse(xml).PureXmlToNotification();
                        }

                        result.Add(log);
                    }
                }
                catch (Exception ex)
                {
                    throw new InvalidOperationException("Failed to ReadPushMessageLog", ex);
                }
                finally
                {
                    reader.Close();
                }
            }

            return result;
        }

        /// <summary>
        /// Reads the push message status.
        /// </summary>
        /// <param name="reader">The reader.</param>
        /// <returns>List{PushMessageStatus}.</returns>
        protected static List<PushMessageStatus> ReadPushMessageStatus(SqlDataReader reader)
        {
            List<PushMessageStatus> result = new List<PushMessageStatus>();

            if (reader != null)
            {
                try
                {
                    while (reader.Read())
                    {
                        result.Add(new PushMessageStatus
                        {
                            MessageId = reader["MessageId"].DBToNullableInt32(),
                            AndroidDeliveredCount = reader["ResponseMessage"].DBToNullableInt32(),
                            ApplePushNotificationDeliveredCount = reader["ApplePushNotificationDeliveredCount"].DBToNullableInt32()
                        });
                    }
                }
                catch (Exception ex)
                {
                    throw ex;
                }
                finally
                {
                    reader.Close();
                }
            }

            return result;
        }
        /// <summary>
        /// Generates the SQL sp parameter.
        /// </summary>
        /// <param name="columnName">Name of the column.</param>
        /// <param name="parameterObject">The parameter object.</param>
        /// <returns>SqlParameter.</returns>
        protected static SqlParameter GenerateSqlSpParameter(string columnName, object parameterObject)
        {
            object obj = parameterObject;

            if (parameterObject != null && parameterObject is XElement)
            {
                obj = parameterObject.ToString();
            }

            return new SqlParameter("@" + columnName.Trim(), parameterObject == null ? Convert.DBNull : obj);
        }
    }
}
